package com.jerry.orm;

import com.jerry.orm.annotation.Delete;
import com.jerry.orm.annotation.Insert;
import com.jerry.orm.annotation.Select;
import com.jerry.orm.annotation.Update;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 负责业务层数据读取数据库的操作
 * 与JDBC建立连接和调用Handler处理参数和结果
 */
public class SqlSession {
    private Handler handler = new Handler();

    //通过ConfigurationReader读取properties文件,根据Key到value
    private String driver = ConfigurationReader.getValue("driver");
    private String url = ConfigurationReader.getValue("dburl");
    private String username = ConfigurationReader.getValue("user");
    private String password = ConfigurationReader.getValue("password");

    //=================设计一个方法,可以在sqlsession类中产生一个代理对象=================
    // 分析 需要返参数吗?  需要,我们生产的是代理对象,你要告诉我代理的是谁
    //     需要返回值吗?   需要,返回一个代理对象,这个代理对象其实就是数据业务层接口的子类实现
    public <T> T getMapper(Class clazz) {

        //创建代理对象,newProxyInstance()有三个参数:
        //1,ClassLoader
        //2,加载的类Class[] 通常来说,数组里就装一个Class,但考虑到可能会一次性创建多个代理对象
        //3,具体该如何做事:InvocationHandler
        //ClassLoader
        ClassLoader classLoader = clazz.getClassLoader();
        //要加载的类Class[]
        Class[] classes = new Class[]{clazz};
        //具体的操作
        InvocationHandler invocation = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * 参数讲解:
                 *  proxy:就是代理对象,代替原来的DAO(本例中是userDao)
                 *  method:表示的是DAO中的方法
                 *  args:DAO中方法的参数
                 */
                //这个invoke方法就是代替原来dao做的事情,之前dao层的UserDao会调用增删改查方法
                //在这里,我们根据反射,拿到dao层接口方法上的注解,如果是Insert注解就调用insert方法,以此类推
                //1,解析dao层方法上的注解对象(获取注解的类型以判别调用哪个方法,获取注解中的值,也就是sql语句,还有调用方法时的传参,参数)
                Annotation an = method.getAnnotations()[0];
                //2,获取注解对象的Class类型
                Class<? extends Annotation> annotationType = an.annotationType();
                //3,获取注解中的值(sql语句) 通过注解对象的Class类型来获得
                Method valueMethod = annotationType.getMethod("value");
                String sql = (String) valueMethod.invoke(an);
                Object obj = args == null ? null : args[0];
                if (annotationType == Insert.class)
                    SqlSession.this.update(sql, obj);
                else if (annotationType == Update.class)
                    SqlSession.this.update(sql, obj);
                else if (annotationType == Delete.class)
                    SqlSession.this.update(sql, obj);
                else if (annotationType == Select.class) {
                    //查询比较复杂一些,光根据Select注解是无法确定要调用是单条查询还是多条查询
                    //可以根据方法的返回值来确定: 是对象,调单条,是集合,调多条
                    Class<?> returnType = method.getReturnType();
                    if (returnType == List.class) {
                        //如果是集合,我们就要取出里面的泛型
                        Type type = method.getGenericReturnType();
                        //将这个type还原成可以操作泛型的类
                        ParameterizedType realReturnType = (ParameterizedType) type;
                        //获取ParameterizedType中的泛型类
                        Type[] patterns = realReturnType.getActualTypeArguments();
                        //获取patterns数组的第一个
                        Type patternType = patterns[0];
                        //将Type类型的再转换成Class类型
                        Class resultType = (Class) patternType;
                        return SqlSession.this.selectList(sql,obj,resultType);
                    }else {
                        return SqlSession.this.selectOne(sql,obj,returnType);
                    }
                }else {
                    System.out.println("这个类型框架没有,出现异常,这个类型框架处理不了");
                }
                return null;
            }
        };
        Object obj = Proxy.newProxyInstance(classLoader, classes, invocation);
        //将代理对象返回出去
        return (T) obj;
    }

    /**
     * ⭐此方法使用反射将查询的结果装入Class类中,也就是resultType中  ⭐方式一在最下边,已注释(可用)
     *
     * @param sql        dao层传递的sql语句
     * @param resultType dao层提供的一个Class类,用来让小弟SqlSession中的
     *                   selectList方法通过反射的方式将数据装入resultType的实例中
     * @param obj        dao层传递过来的拼接sql语句问号的参数
     * @param <T>
     * @return
     */
    public <T> T selectOne(String sql, Object obj, Class resultType) throws SQLException {
        return (T) selectList(sql, obj, resultType).get(0);
    }

    /**
     * ⭐此方法使用反射将查询的结果装入Class类中,也就是resultType中	⭐方式一在最下边,已注释(可用)
     *
     * @param sql        dao层传递的sql语句
     * @param resultType dao层提供的一个Class类,用来让小弟SqlSession中的
     *                   selectList方法通过反射的方式将数据装入resultType的实例中
     * @param obj        dao层传递过来的拼接sql语句问号的参数
     * @param <T>
     * @return
     */
    public <T> List<T> selectList(String sql, Object obj, Class resultType) throws SQLException {
        List<T> list = new ArrayList<>();
        try {
            Class.forName(driver);
            SQLAndKey sqlAndKey = handler.parseSQL(sql);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sqlAndKey.getSql());
            if (obj != null) {
                handler.handlerParameter(statement, obj, sqlAndKey.getList());
            }
            ResultSet rs = statement.executeQuery();
            while (rs.next()) {
                //这个过程是让rs里装的数据使用反射的技术转换成对象的过程
                T result = (T) handler.handlerResult(rs, resultType);
                list.add(result);
            }
            rs.close();
            statement.close();
            connection.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return list;
    }

    //===================方式二: #{}方式实现:这样参数只传sql和类型即可=======================

    /**
     * 为了实现sql语句带#{}的功能
     *
     * @param sql = "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
     * @param obj Object类型是为了兼容任何类型
     */
    public void update(String sql, Object obj) {
        if (sql == null || sql == "") return;
        /*
            解析这条特殊的sql: "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
            我们要将#{属性名}中的属性名取出来放入到一个有序集合中,这是顺序的保证,还要将这条sql中的#{}再替换成 ? 问号
            找Handler,让它帮我解析这条sql,然后帮我返回一个有序的集合,和带?问号的sql
            那么这里需要返回两个不同类型的数据,我们可以单独创建一个类: SQLAndKey,就只是来存放这两个返回值
        * */
        SQLAndKey sqlAndKey = handler.parseSQL(sql);
        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sqlAndKey.getSql());
            /**
             * 让statement对象,将带有?问号的sql和obj对象中存储的值,进行完整的拼接
             * 在handler中定义一个小弟方法,让它帮忙完成这个功能
             */
            if (obj != null)
                handler.handlerParameter(statement, obj, sqlAndKey.getList());
            //上部执行完以后就可以执行啦
            statement.executeUpdate();
            statement.close();
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
